<!DOCTYPE html>
  <meta charset=utf-8>
  <meta name=viewport content=width=device-width,initial-scale=1,user-scalable=no>
  <title>
    BrowserGap &mdash; Point field demo
  </title>
  <style>
    :root, body {
      height: 200vh;
      width: 100%;
      margin: 0;
    }

    canvas {
      position: fixed;
      top: 0;
      width: 100%;
      height: 100vh;
      user-select: none;
      background: linear-gradient(to right top, navy, dodgerblue);
    }

    aside {
      position: relative;
      z-index: 2;
      padding: 2em;
      color: white;
      font-size: larger;
      font-family: Verdana, Helvetica, Arial, system-ui, sans;
      pointer-events: none;
    }
  </style>
  <canvas>
    <script>
      const frameReady = () => new Promise(res => window.requestAnimationFrame(res))
      const sleep = ms => new Promise(res => setTimeout(res, ms));
      const ALTERP = 0.618;
      const DTHRESHOLD = 0.618;
      const FRAMEDURATION = 40;
      const F = Math.floor(1000/FRAMEDURATION);
      const UPDATEP = 2;
      const XAVAL = 7;
      const YAVAL = 3;
      const SIZE = [3,3];
      const N = 71;
      const NN = 1;
      const MAXNN = Math.ceil(Math.sqrt(N));
      const MAXD = Math.min(window.innerWidth**2, window.innerHeight**2)/9;
      const C = document.currentScript.parentElement;     
      const S = C.getContext('2d');
      const width = window.innerWidth;
      const height = window.innerHeight;

      let frameCount = 0;
      let nnds = +1;
      let nn = Math.round(Math.sqrt(MAXNN * NN));
      let allPairsCache;

      C.width = width;
      C.height = height;
      S.fillStyle = 'white';
      S.strokeStyle = 'white';

      C.addEventListener('click', () => {
        nn += nnds;
        if ( nn > MAXNN ) {
          nnds *= -1;
        }
        if ( nn < NN ) {
          nnds *= -1;
        }
      });

      drawLoop();

      async function drawLoop() {
        const Points = createPoints(N);
        while(true) {
          await sleep(FRAMEDURATION);
          await frameReady(); 
          S.clearRect(0,0,width,height);
          update(Points);
          draw(Points); 
          drawTrianglesOnClosest(Points, frameCount);
          frameCount++;
        }
      }

      function update(P) {
        P.forEach(updateA);
      }

      function draw(P) {
        P.forEach(({x,y}) => {
          //S.fillRect(x,y,...SIZE)
          S.beginPath();
          S.arc(x,y,SIZE[0],0,Math.PI*2);
          S.closePath();
          S.fill();
        });
      }

      function drawTrianglesOnClosest(Points, frameNo) {
        const found = new Map();

        if ( ! allPairsCache ) {
          allPairsCache = allPairs(Points)
            //.map(distance);
        }
        allPairsCache
          .map(frameNo % UPDATEP == 0 ? ranDistance: X => X)
          //.map(ranDistance)
          .sort(([{}, {}, d1], [{}, {}, d2]) => d1 - d2)
          .filter(([P1,P2,d]) => {
            const ok = (!found.has(P1) || found.get(P1) <= nn) && (!found.has(P2) || found.get(P2) <= nn); 
            if ( ok ) {
              found.set(P1, (found.get(P1) || 0)+1);
              found.set(P2, (found.get(P2) || 0)+1);
            }
            return ok && d < MAXD;
          })
          .forEach(([P1,P2]) => {
            S.beginPath();
            S.moveTo(P1.x, P1.y);
            S.lineTo(P2.x, P2.y);
            S.stroke();
            S.closePath();
          });

          /**
            .reduce((T, p, i, P) => {
              const last = T[T.length-1];
              if ( last.length == 3 ) {
                T.push([p]);
              } else {
                last.push(p);
              }
            }, []);
          **/
      }

      function updateA(A) {
        const randomX = (Math.random() * XAVAL) - XAVAL/2;
        const randomY = (Math.random() * YAVAL) - YAVAL/2;
        A.x += randomX;
        A.y += randomY;
        return A;
      }

      function createPoints(n) {
        const P = [];
        for( let i = 0; i < n; i++ ) {
          const Point = {
            x: random(width),
            y: random(height),
            V: {
              x: random(width/20),
              y: random(height/20)
            },
            A: updateA({x:XAVAL,y:YAVAL})
          };
          P.unshift(Point);
        }
        return P;
      }

      function random(max) {
        return Math.random() * max;
      }

      function allPairs(A) {
        const P = [];
        A.forEach((a,i) => {
          for( let j = i + 1; j < A.length; j++ ) {
            const [{x:x1,y:y1},{x:X1,y:Y1}] = [a,A[j]];
            P.push([a,A[j],0]);
          }
        });
        return P;
      }

      function distance(P) {
        const [{x:x1,y:y1},{x:X1,y:Y1}] = P;
        const d = ((X1-x1)**2+(Y1-y1)**2);
        P[2] = d;
        return P;
      }

      function ranDistance(P) {
        const [{x:x1,y:y1},{x:X1,y:Y1}] = P;
        const d = ((X1-x1)**2+(Y1-y1)**2);
        if ( Math.random() < ALTERP ) {
          if ( Math.abs(P[2] - d)/d > DTHRESHOLD ) {
            P[2] = d;
          }
        }
        return P;
      }
    </script>
  </canvas>
  <aside>
    <h1>Point field test</h1>
    <p>Could also add colors to each node / point, and sum colors with neighbour to determine line color.</p>
    <p>Or even make a linear gradient with direction equal to line direction to move between node colors.</p>
  </aside>
